package pl.wr.math.number;

import pl.wr.math.Component;
import pl.wr.math.utils.Util;

/**
 * Data type for ordinary fraction.
 * 
 * @version 1.0
 * @author wieslaw.rodak
 * 
 */
public final class Fraction extends Number implements Component {

    /** A fraction representing "0". */
    public static final Fraction ZERO = new Fraction(0);

    /** A fraction representing "1". */
    public static final Fraction ONE = new Fraction(1);

    /** A fraction representing "-1". */
    public static final Fraction MINUS_ONE = new Fraction(-1);

    /** A fraction representing "1/2". */
    public static final Fraction ONE_HALF = new Fraction(1, 2);

    /** A fraction representing "-1/2". */
    public static final Fraction MINUS_ONE_HALF = new Fraction(-1, 2);

    /** A fraction representing "1/3". */
    public static final Fraction ONE_THIRD = new Fraction(1, 3);

    /** A fraction representing "-1/3". */
    public static final Fraction MINUS_ONE_THIRD = new Fraction(-1, 3);

    /** A fraction representing "2/3". */
    public static final Fraction TWO_THIRDS = new Fraction(2, 3);

    /** A fraction representing "-2/3". */
    public static final Fraction MINUS_TWO_THIRDS = new Fraction(-2, 3);

    /** A fraction representing "1/4". */
    public static final Fraction ONE_QUARTER = new Fraction(1, 4);

    /** A fraction representing "2/4". */
    public static final Fraction TWO_QUARTERS = new Fraction(2, 4);

    /** A fraction representing "3/4". */
    public static final Fraction THREE_QUARTERS = new Fraction(3, 4);

    /** A fraction representing "1/5". */
    public static final Fraction ONE_FIFTH = new Fraction(1, 5);

    /** A fraction representing "2/5". */
    public static final Fraction TWO_FIFTHS = new Fraction(2, 5);

    /** A fraction representing "3/5". */
    public static final Fraction THREE_FIFTHS = new Fraction(3, 5);

    /** A fraction representing "4/5". */
    public static final Fraction FOUR_FIFTHS = new Fraction(4, 5);

    /** A fraction representing "√2". */
    // public static final Fraction ROOT_2 = new Fraction(1.4142135623730951);
    public static final Fraction ROOT_2 = new Fraction(1767766952966369L, 1250000000000000L);

    /** A fraction representing "√3". */
    // public static final Fraction ROOT_3 = new Fraction(1.7320508075688772);
    public static final Fraction ROOT_3 = new Fraction(4330127018922193L, 2500000000000000L);

    /**
     * The uncertainty = 0
     */
    public static final int EXACTLY = 0;

    /**
     * The uncertainty = unknown
     */
    public static final int UNKNOWN = -1;

    private static final long safeNumber = (long) Math.sqrt(Long.MAX_VALUE);

    /**
     * Field serialVersionUID. (value is -5481293843905644207)
     */
    private static final long serialVersionUID = -5481293843905644207L;

    /**
     * numerator of fraction
     */
    private long numerator;

    /**
     * denominator of fraction
     */
    private long denominator;

    /**
     * decimal power 10<sup>x</sup>
     */
    private int decimalPower;

    /**
     * standard uncertainty
     */
    private int uncertainty;

    /**
     * reduce the length numerator and denominator and check scope of numbers
     */
    private boolean reduceAndScope = true;

    /**
     * Fraction new instance
     */
    public Fraction() {
    }

    /**
     * @param total
     *            - total number
     */
    public Fraction(int total) {
        this((long) total, 1L, 0);
    }

    /**
     * @param total
     *            - total number
     */
    public Fraction(long total) {
        this(total, 1L, 0);
    }

    /**
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     */
    public Fraction(int numerator, int denominator) {
        this(numerator, denominator, 0);
    }

    /**
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(int numerator, int denominator, int decimalPower) {
        this((long) numerator, (long) denominator, decimalPower);
    }

    /**
     * @param total
     *            - total number
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(int total, int numerator, int denominator, int decimalPower) {
        this(total * denominator + numerator, denominator, decimalPower);
    }

    /**
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     */
    public Fraction(long numerator, long denominator) {
        this(numerator, denominator, 0);
    }

    /**
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(long numerator, long denominator, int decimalPower) {
        this.numerator = numerator;
        this.denominator = denominator;
        this.decimalPower = decimalPower;
        uncertainty = EXACTLY;
    }

    /**
     * @param total
     *            - total number
     * @param numerator
     *            - numerator of fraction
     * @param denominator
     *            - denominator of fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(long total, long numerator, long denominator, int decimalPower) {
        this(total * denominator + numerator, denominator, decimalPower);
    }

    /**
     * @param decimalFraction
     *            - decimal fraction
     */
    public Fraction(float decimalFraction) {
        this((double) decimalFraction, 0, EXACTLY);
    }

    /**
     * @param decimalFraction
     *            - decimal fraction
     */
    public Fraction(double decimalFraction) {
        this(decimalFraction, 0, EXACTLY);
    }

    /**
     * @param decimalFraction
     *            - decimal fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(float decimalFraction, int decimalPower) {
        this((double) decimalFraction, decimalPower, EXACTLY);
    }

    /**
     * @param decimalFraction
     *            - decimal fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public Fraction(double decimalFraction, int decimalPower) {
        this(decimalFraction, decimalPower, EXACTLY);
    }

    /**
     * @param decimalFraction
     *            - decimal fraction
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     * @param uncertainty
     *            - standard uncertainty
     */
    public Fraction(double decimalFraction, int decimalPower, int uncertainty) {
        final long[] c = Util.decimalToCommon(decimalFraction);
        numerator = c[0];
        denominator = c[1];
        this.decimalPower = decimalPower + (int) c[2];
        this.uncertainty = uncertainty;
    }

    /**
     * 
     * @return - numerator of fraction
     */
    public long getNumerator() {
        return numerator;
    }

    /**
     * @param numerator
     *            - numerator of fraction
     */
    public void setNumerator(final long numerator) {
        this.numerator = numerator;
    }

    /**
     * 
     * @return - denominator of fraction
     */
    public long getDenominator() {
        return denominator;
    }

    /**
     * @param denominator
     *            - denominator of fraction
     */
    public void setDenominator(final long denominator) {
        if (0 == denominator)
            throw new ArithmeticException("denominator can not be zero");
        this.denominator = denominator;
    }

    /**
     * 
     * @return - decimal power 10<sup>x</sup>
     */
    public int getDecimalPower() {
        return decimalPower;
    }

    /**
     * @param decimalPower
     *            - decimal power 10<sup>x</sup>
     */
    public void setDecimalPower(final int decimalPower) {
        this.decimalPower = decimalPower;
    }

    /**
     * @return standard uncertainty
     */
    public int getUncertainty() {
        return uncertainty;
    }

    /**
     * @param uncertainty
     *            - standard uncertainty
     */
    public void setUncertainty(final int uncertainty) {
        this.uncertainty = uncertainty;
    }

    /**
     * 
     * @return the reduce and scope state
     */
    public boolean isReduceAndScope() {
        return reduceAndScope;
    }

    /**
     * @param reduceAndScope
     *            the reduce and scope state to set
     */
    public void setReduceAndScope(final boolean reduceAndScope) {
        this.reduceAndScope = reduceAndScope;
    }

    /**
     * Add two Fraction numbers
     * 
     * @param a
     *            - Fraction a
     * @param b
     *            - Fraction b
     * 
     * @return new Fraction c
     */
    public static Fraction add(final Fraction a, final Fraction b) {

        final boolean reduceAndScope = a.isReduceAndScope() && b.isReduceAndScope();

        if (reduceAndScope) {
            if (exceededScope(a, b, false /* add */)) {
                Fraction ex = new Fraction(a.doubleValue() + b.doubleValue());
                ex.setUncertainty(UNKNOWN);
                return ex;
            }
        }

        final Fraction a1 = Fraction.clone(a);
        final Fraction b1 = Fraction.clone(b);
        final Fraction c = new Fraction();

        if (a1.getDecimalPower() != b1.getDecimalPower()) {

            if (a1.getDecimalPower() > b1.getDecimalPower()) {
                a1.setNumerator((long) (a1.getNumerator() * powerValue(a1.getDecimalPower() - b1.getDecimalPower())));
                a1.setDecimalPower(b1.getDecimalPower());
            } else {
                b1.setNumerator((long) (b1.getNumerator() * powerValue(b1.getDecimalPower() - a1.getDecimalPower())));
                b1.setDecimalPower(a1.getDecimalPower());
            }

        }

        c.setDecimalPower(a1.getDecimalPower());

        if (a1.getDenominator() == b1.getDenominator()) {
            c.setNumerator(a1.getNumerator() + b1.getNumerator());
            c.setDenominator(a1.getDenominator());
        } else {
            final long commonDenominator = Util.gcf(a1.getDenominator(), b1.getDenominator());
            final long aMultiplier = commonDenominator / a1.getDenominator();
            final long bMultiplier = commonDenominator / b1.getDenominator();
            c.setNumerator(a1.getNumerator() * aMultiplier + b1.getNumerator() * bMultiplier);
            c.setDenominator(commonDenominator);
        }

        // reduce the length numerator and denominator
        if (reduceAndScope)
            reduce(c);

        return c;
    }

    /**
     * Subtract two Fraction numbers
     * 
     * @param a
     *            - Fraction a
     * @param b
     *            - Fraction b
     * 
     * @return new Fraction c
     */
    public static Fraction subtract(final Fraction a, final Fraction b) {

        final boolean reduceAndScope = a.isReduceAndScope() && b.isReduceAndScope();

        if (reduceAndScope) {
            if (exceededScope(a, b, false /* subtract */)) {
                Fraction ex = new Fraction(a.doubleValue() - b.doubleValue());
                ex.setUncertainty(UNKNOWN);
                return ex;
            }
        }

        final Fraction a1 = Fraction.clone(a);
        final Fraction b1 = Fraction.clone(b);
        final Fraction c = new Fraction();

        if (a1.getDecimalPower() != b1.getDecimalPower()) {

            if (a1.getDecimalPower() > b1.getDecimalPower()) {
                a1.setNumerator((long) (a1.getNumerator() * powerValue(a1.getDecimalPower() - b1.getDecimalPower())));
                a1.setDecimalPower(b1.getDecimalPower());
            } else {
                b1.setNumerator((long) (b1.getNumerator() * powerValue(b1.getDecimalPower() - a1.getDecimalPower())));
                b1.setDecimalPower(a1.getDecimalPower());
            }

        }

        c.setDecimalPower(a1.getDecimalPower());

        if (a1.getDenominator() == b1.getDenominator()) {
            c.setNumerator(a1.getNumerator() - b1.getNumerator());
            c.setDenominator(a1.getDenominator());
        } else {
            final long commonDenominator = Util.gcf(a1.getDenominator(), b1.getDenominator());
            final long aMultiplier = commonDenominator / a1.getDenominator();
            final long bMultiplier = commonDenominator / b1.getDenominator();
            c.setNumerator(a1.getNumerator() * aMultiplier - b1.getNumerator() * bMultiplier);
            c.setDenominator(commonDenominator);
        }

        // reduce the length numerator and denominator
        if (reduceAndScope)
            reduce(c);

        return c;
    }

    /**
     * Multiply two Fraction numbers
     * 
     * @param a
     *            - Fraction a
     * @param b
     *            - Fraction b
     * 
     * @return new Fraction c
     */
    public static Fraction multiply(final Fraction a, final Fraction b) {

        final boolean reduceAndScope = a.isReduceAndScope() && b.isReduceAndScope();

        if (reduceAndScope) {
            if (exceededScope(a, b, true /* multiply */)) {
                Fraction ex = new Fraction(a.doubleValue() * b.doubleValue());
                ex.setUncertainty(UNKNOWN);
                return ex;
            }
        }

        final Fraction c = new Fraction();
        c.setDecimalPower(a.getDecimalPower() + b.getDecimalPower());

        final long numerator = a.getNumerator() * b.getNumerator();
        final long denominator = a.getDenominator() * b.getDenominator();

        c.setNumerator(numerator);
        c.setDenominator(denominator);

        // reduce the length numerator and denominator
        if (reduceAndScope)
            reduce(c);

        return c;
    }

    public static Fraction fastMultiply(final Fraction a, final Fraction b) {

        if (a.getNumerator() < safeNumber && a.getDenominator() < safeNumber && b.getNumerator() < safeNumber
                && b.getDenominator() < safeNumber) {

            final Fraction c = new Fraction();
            c.setDecimalPower(a.getDecimalPower() + b.getDecimalPower());

            final long numerator = a.getNumerator() * b.getNumerator();
            final long denominator = a.getDenominator() * b.getDenominator();

            c.setNumerator(numerator);
            c.setDenominator(denominator);

            return c;
        } else {
            return multiply(a, b);
        }

    }

    /**
     * Divide two Fraction numbers
     * 
     * @param a
     *            - Fraction a
     * @param b
     *            - Fraction b
     * 
     * @return new Fraction c
     */
    public static Fraction divide(final Fraction a, final Fraction b) {

        final boolean reduceAndScope = a.isReduceAndScope() && b.isReduceAndScope();

        if (reduceAndScope) {
            if (exceededScope(a, b, true /* divide */)) {
                Fraction ex = new Fraction(a.doubleValue() / b.doubleValue());
                ex.setUncertainty(UNKNOWN);
                return ex;
            }
        }

        final Fraction c = new Fraction();
        c.setDecimalPower(a.getDecimalPower() - b.getDecimalPower());

        final long numerator = a.getNumerator() * b.getDenominator();
        final long denominator = a.getDenominator() * b.getNumerator();

        c.setNumerator(numerator);
        c.setDenominator(denominator);

        // reduce the length numerator and denominator
        if (reduceAndScope)
            reduce(c);

        return c;
    }

    /**
     * Method intValue.
     * 
     * @return int
     */
    @Override
    public int intValue() {
        return (int) (((double) numerator / (double) denominator) * powerValue());
    }

    /**
     * Method longValue.
     * 
     * @return long
     */
    @Override
    public long longValue() {
        return (long) (((double) numerator / (double) denominator) * powerValue());
    }

    /**
     * Method floatValue.
     * 
     * @return float
     */
    @Override
    public float floatValue() {
        return (float) (((float) numerator / (float) denominator) * powerValue());
    }

    /**
     * Method doubleValue.
     * 
     * @return double
     */
    @Override
    public double doubleValue() {
        return ((double) numerator / (double) denominator) * powerValue();
    }

    /**
     * Check if number is zero (numerator = 0)
     * 
     * 
     * @return true/false
     */
    public boolean isZero() {
        return (0 == numerator) ? true : false;
    }

    /**
     * Method exceededScope.
     * 
     * @param a
     * @param b
     * @param multi
     * @return
     */
    private static boolean exceededScope(final Fraction a, final Fraction b, final boolean multi) {

        if (multi) {
            if (a.getNumerator() > Long.MAX_VALUE / b.getNumerator()
                    || a.getDenominator() > Long.MAX_VALUE / b.getDenominator()) {
                return true;
            }
        } else {
            if (Math.abs(a.getNumerator()) > Long.MAX_VALUE - Math.abs(b.getNumerator())
                    || Math.abs(a.getDenominator()) > Long.MAX_VALUE - Math.abs(b.getDenominator())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Method powerValue.
     * 
     * @return double
     */
    private double powerValue() {
        return Math.pow(10, decimalPower);
    }

    /**
     * Method powerValue.
     * 
     * @param pow
     *            int
     * @return double
     */
    private static double powerValue(final int pow) {
        return Math.pow(10, pow);
    }

    /**
     * Method toString.
     * 
     * @return String
     */
    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder(32);
        builder.append("Fraction [numerator=");
        builder.append(numerator);
        builder.append(", denominator=");
        builder.append(denominator);
        builder.append(", decimalPower=");
        builder.append(decimalPower);
        builder.append(", uncertainty=");
        builder.append(uncertainty);
        builder.append(']');
        return builder.toString();
    }

    /**
     * @param oryginal
     *            - oryginal fraction
     * @return copy of oryginal
     */
    public static Fraction clone(final Fraction oryginal) {

        final Fraction copy = new Fraction(oryginal.getNumerator(), oryginal.getDenominator(),
                oryginal.getDecimalPower());
        copy.setUncertainty(oryginal.getUncertainty());
        copy.setReduceAndScope(oryginal.isReduceAndScope());

        return copy;
    }

    /**
     * Reduce numerator and denominator by greatest common divisor.
     * 
     * @param oryginal
     *            - Fraction number
     */
    public static void reduce(final Fraction oryginal) {

        final long divisor = Util.gcd(oryginal.getNumerator(), oryginal.getDenominator());
        if (divisor > 1) {
            oryginal.setNumerator(oryginal.getNumerator() / divisor);
            oryginal.setDenominator(oryginal.getDenominator() / divisor);
        }
    }

    /**
     * @return
     */
    public long round() {
        return Math.round(doubleValue());
    }

    /**
     * Return the additive inverse of this fraction.
     * 
     * @return the negation of this fraction.
     */
    public Fraction negate() {
        return new Fraction(-numerator, denominator);
    }

    /**
     * Return the multiplicative inverse of this fraction.
     * 
     * @return the reciprocal fraction
     */
    public Fraction reciprocal() {
        return new Fraction(denominator, numerator);
    }
}
